package launcher;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * These classes are adapted from org.eclipse.equinox.servletbridge_1.0.0.v20070523,
 * to help running equinox as a headless non-app in background.<br>
 * 
 * Copy the jar into your equinox home, run:
 * java -jar launcher.jar &
 * or
 * nohup java -jar launcher.jar & on linux
 * <br>
 * 
 * You must hava a file named config.ini in directory "configuration", and set these parameters:<br>
 * <ul>
 *   <li>eclipse.ignoreApp=true</li>
 *   <li>osgi.noShutdown=true</li>
 * </ul>  
 */
public class Main {

	private static final String FRAMEWORK_BUNDLE_NAME = "org.eclipse.osgi";
	private static final String OSGI_FRAMEWORK = "osgi.framework";
	private static final String OSGI_INSTANCE_AREA = "osgi.instance.area";
	private static final String OSGI_CONFIGURATION_AREA = "osgi.configuration.area";
	private static final String OSGI_INSTALL_AREA = "osgi.install.area";
	private static final String CONFIG_INI = "config.ini";
	private static final String STARTER = "org.eclipse.core.runtime.adaptor.EclipseStarter";
	private static ClassLoader frameworkContextClassLoader;
	private static URLClassLoader frameworkClassLoader;

	public static void main(String[] args) {
		new Main().start();
	}
	
	private void start() {
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				stopFramework();
			}
		});

		try {
			Map<String, String> parameters = init();
			startFramework(parameters);
		} catch (Exception e) {
			Log.log("start equinox failed", e);
		}
		synchronized (this) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				
			}
		}
	}

	@SuppressWarnings("unchecked")
	private Map<String, String> init() throws Exception {
		Map<String, String> parameters = new HashMap<String, String>();
		String installArea = System.getProperty(OSGI_INSTALL_AREA);
		if (installArea == null) {
			File dir = new File (".");
			installArea = dir.getCanonicalPath();
		}
		
		Log.log(OSGI_INSTALL_AREA + "=" + installArea);
		parameters.put(OSGI_INSTALL_AREA, installArea);
		
		String frameworkBundle = searchFor(FRAMEWORK_BUNDLE_NAME, new File(installArea, "plugins").getCanonicalPath());
		if (installArea == null)
			throw new RuntimeException("Could not find framework");
		parameters.put(OSGI_FRAMEWORK, new File(frameworkBundle).toURI().toURL().toExternalForm());
		
		String confArea = System.getProperty(OSGI_CONFIGURATION_AREA);
		if (confArea == null) {
			confArea = new File(installArea, "configuration").getCanonicalPath();
		}
		parameters.put(OSGI_CONFIGURATION_AREA, confArea);
		
		String instArea = System.getProperty(OSGI_INSTANCE_AREA);
		if (instArea == null) {
			instArea = new File(confArea, "org.eclipse.osgi").getCanonicalPath();
		}
		parameters.put(OSGI_INSTANCE_AREA, instArea);
		
		Map initialPropertyMap = new HashMap();
		Properties launchProperties = loadProperties(new File(confArea, CONFIG_INI).getCanonicalPath());
		for (Iterator it = launchProperties.entrySet().iterator(); it.hasNext();) {
			Map.Entry entry = (Map.Entry) it.next();
			initialPropertyMap.put(entry.getKey(), entry.getValue());
		}
		return parameters;
	}
	
	private Properties loadProperties(String resource) throws Exception {
		Properties result = new Properties();
		InputStream in = null;
		try {
			URL location = new URL("file://" + resource);
			if (location != null) {
				in = location.openStream();
				result.load(in);
			}
		} catch (Exception e) {
			throw e;
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) {
					// ignore
				}
			}
		}
		return result;
	}
	
	private String searchFor(final String target, String start) {
		FileFilter filter = new FileFilter() {
			public boolean accept(File candidate) {
				return candidate.getName().equals(target) || candidate.getName().startsWith(target + "_");
			}
		};
		File[] candidates = new File(start).listFiles(filter);
		if (candidates == null)
			return null;
		String[] arrays = new String[candidates.length];
		for (int i = 0; i < arrays.length; i++) {
			arrays[i] = candidates[i].getName();
		}
		int result = findMax(arrays);
		if (result == -1)
			return null;
		return candidates[result].getAbsolutePath().replace(File.separatorChar, '/') + (candidates[result].isDirectory() ? "/" : "");//$NON-NLS-2$
	}

	private int findMax(String[] candidates) {
		int result = -1;
		Object maxVersion = null;
		for (int i = 0; i < candidates.length; i++) {
			String name = candidates[i];
			String version = ""; // Note: directory with version suffix is always > than directory without version suffix
			int index = name.indexOf('_');
			if (index != -1)
				version = name.substring(index + 1);
			Object currentVersion = getVersionElements(version);
			if (maxVersion == null) {
				result = i;
				maxVersion = currentVersion;
			} else {
				if (compareVersion((Object[]) maxVersion, (Object[]) currentVersion) < 0) {
					result = i;
					maxVersion = currentVersion;
				}
			}
		}
		return result;
	}

	/**
	 * Compares version strings. 
	 * @param left 
	 * @param right 
	 * @return result of comparison, as integer;
	 * <code><0</code> if left < right;
	 * <code>0</code> if left == right;
	 * <code>>0</code> if left > right;
	 */
	private int compareVersion(Object[] left, Object[] right) {

		int result = ((Integer) left[0]).compareTo((Integer) right[0]); // compare major
		if (result != 0)
			return result;

		result = ((Integer) left[1]).compareTo((Integer) right[1]); // compare minor
		if (result != 0)
			return result;

		result = ((Integer) left[2]).compareTo((Integer) right[2]); // compare service
		if (result != 0)
			return result;

		return ((String) left[3]).compareTo((String) right[3]); // compare qualifier
	}

	/**
	 * Do a quick parse of version identifier so its elements can be correctly compared.
	 * If we are unable to parse the full version, remaining elements are initialized
	 * with suitable defaults.
	 * @param version 
	 * @return an array of size 4; first three elements are of type Integer (representing
	 * major, minor and service) and the fourth element is of type String (representing
	 * qualifier). Note, that returning anything else will cause exceptions in the caller.
	 */
	private Object[] getVersionElements(String version) {
		if (version.endsWith(".jar"))
			version = version.substring(0, version.length() - 4);
		Object[] result = {new Integer(0), new Integer(0), new Integer(0), ""};
		StringTokenizer t = new StringTokenizer(version, ".");
		String token;
		int i = 0;
		while (t.hasMoreTokens() && i < 4) {
			token = t.nextToken();
			if (i < 3) {
				// major, minor or service ... numeric values
				try {
					result[i++] = new Integer(token);
				} catch (Exception e) {
					// invalid number format - use default numbers (0) for the rest
					break;
				}
			} else {
				// qualifier ... string value
				result[i++] = token;
			}
		}
		return result;
	}

	@SuppressWarnings("unchecked")
	public synchronized void startFramework(Map initalPropertyMap) {
		if (frameworkClassLoader != null) {
			Log.log("Framework is already started");
			return;
		}

		ClassLoader original = Thread.currentThread().getContextClassLoader();
		try {
			System.setProperty("osgi.framework.useSystemProperties", "false");

			URL[] osgiURLArray = {new URL((String) initalPropertyMap.get(OSGI_FRAMEWORK))};
			frameworkClassLoader = new ChildFirstURLClassLoader(osgiURLArray, this.getClass().getClassLoader());
			Class clazz = frameworkClassLoader.loadClass(STARTER);

			Method setInitialProperties = clazz.getMethod("setInitialProperties", new Class[] {Map.class});
			setInitialProperties.invoke(null, new Object[] {initalPropertyMap});

			Method runMethod = clazz.getMethod("startup", new Class[] {String[].class, Runnable.class});
			runMethod.invoke(null, new Object[] {new String[]{}, null});

			frameworkContextClassLoader = Thread.currentThread().getContextClassLoader();
		} catch (InvocationTargetException ite) {
			Throwable t = ite.getTargetException();
			if (t == null)
				t = ite;
			Log.log("Error while starting Framework", t);
			throw new RuntimeException(t.getMessage());
		} catch (Exception e) {
			Log.log("Error while starting Framework", e);
			throw new RuntimeException(e.getMessage());
		} finally {
			Thread.currentThread().setContextClassLoader(original);
		}
	}

	/**
	 * stop is used to "shutdown" the framework and make it available for garbage collection.
	 * The default implementation also has special handling for Apache Commons Logging to "release" any
	 * resources associated with the frameworkContextClassLoader.
	 */
	@SuppressWarnings("unchecked")
	public synchronized void stopFramework() {
		if (frameworkClassLoader == null) {
			Log.log("Framework is already shutdown");
			return;
		}

		ClassLoader original = Thread.currentThread().getContextClassLoader();
		try {
			Class clazz = frameworkClassLoader.loadClass(STARTER);
			Method method = clazz.getDeclaredMethod("shutdown", (Class[]) null);
			Thread.currentThread().setContextClassLoader(frameworkContextClassLoader);
			method.invoke(clazz, (Object[]) null);

			// ACL keys its loggers off of the ContextClassLoader which prevents GC without calling release. 
			// This section explicitly calls release if ACL is used.
			try {
				clazz = this.getClass().getClassLoader().loadClass("org.apache.commons.logging.LogFactory");
				method = clazz.getDeclaredMethod("release", new Class[] {ClassLoader.class});
				method.invoke(clazz, new Object[] {frameworkContextClassLoader});
			} catch (ClassNotFoundException e) {
				// ignore, ACL is not being used
			}
		} catch (Exception e) {
			Log.log("Error while stopping Framework", e);
			return;
		} finally {
			frameworkClassLoader = null;
			frameworkContextClassLoader = null;
			Thread.currentThread().setContextClassLoader(original);
		}
	}
}